import { randomUUID } from "crypto";
import JSEncrypt from "jsencrypt";

const DEBUG = false;

export type Cookies = Record<string, string[]>;

// @ts-ignore
if (global && !global.window) global.window = {};

export function extractSetCookie(setCookieOrSetCookies: string | string[]): Cookies {
    if (Array.isArray(setCookieOrSetCookies)) {
        const result: Record<string, string[]> = {};
        setCookieOrSetCookies.forEach((setCookie) =>
            mergeCookies(result, extractSetCookie(setCookie)),
        );
        return result;
    }
    const result: Record<string, string[]> = {};
    for (const [key, value] of setCookieOrSetCookies
        .split(";")
        .map((rawKV) => rawKV.trim())
        .map((rawKV) => rawKV.split("="))) {
        if (result[key] === undefined) {
            result[key] = [value];
            continue;
        }
        result[key].push(value);
    }
    return result;
}

export function mergeCookies(base: Cookies, addition: Cookies) {
    for (const [key, values] of Object.entries(addition)) {
        if (base[key] === undefined) {
            base[key] = values.slice();
            continue;
        }
        for (const value of values) {
            if (value in base[key]) continue;
            base[key].push(value);
        }
    }
    return base;
}

export function getRawCookiesString(cookies: Cookies): string {
    return Object.entries(cookies)
        .map(([key, values]) =>
            values.map((value) => `${key}=${value}`).join("; "),
        )
        .join("; ");
}

export async function fetchWithCookiesAndMerge(
    url: string,
    cookies: Cookies,
    redirectDepth: number = 0,
    method: string = "get",
    headers?: Object,
    body?: string,
): Promise<{
    response: Response;
    redirect?: string;
}> {
    const response = await fetch(url, {
        method: method,
        redirect: "manual",
        headers: Object.assign(
            {
                Cookie: getRawCookiesString(cookies),
            },
            headers ?? {},
        ) as unknown as HeadersInit,
        body,
    });
    mergeCookies(cookies, extractSetCookie(response.headers.getSetCookie()));
    const redirect: string | undefined =
        response.headers.get("location") ?? undefined;
    if (redirect && redirectDepth > 0) {
        return fetchWithCookiesAndMerge(redirect, cookies, redirectDepth - 1);
    }

    DEBUG &&
        console.log(
            `${method.toUpperCase()} ${url}: ${response.status} ${
                response.statusText
            }, content:\n${
                redirect ? redirect : await response.clone().text()
            }`,
        );

    return {
        response,
        redirect,
    };
}

const PUBKEY =
    "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFKLeH1AOOCNi24fyafTvAQYDuthH3UIlvBU5DxBz+uP6UFWclbk707Hwnuq9tI4I78vzJdmw2F5bu+7wqZeI5OczdEgIjxdEzgLX1QQvk2y4Eaxv6ui9h0/lXoTqHW7gtAcmyLBexl+v/ig22fLfG6XKvXThdTZ2jWRF5ws/KlQIDAQABgwQco1KRMDSmXSMkDwIDAQAB";

export async function login(loginname: string, pwd: string): Promise<Cookies> {
    const cookies: Cookies = {};
    await fetchWithCookiesAndMerge(
        "https://sso.bestsch.com/old/index.html",
        cookies,
    );
    const encrypt = new JSEncrypt();
    encrypt.setPublicKey(PUBKEY);
    const body = await (
        await fetchWithCookiesAndMerge(
            (() => {
                const nextURL = new URL("https://sso.bestsch.com/api/Login");
                nextURL.searchParams.set(
                    "loginname",
                    encrypt.encrypt(loginname) || "",
                );
                nextURL.searchParams.set("pwd", encrypt.encrypt(pwd) || "");
                nextURL.searchParams.set("captcha", "");
                nextURL.searchParams.set("service", "");
                return nextURL.toString();
            })(),
            cookies,
        )
    ).response.json();
    if (body.data.url === undefined) throw new Error("login failed!");

    await fetchWithCookiesAndMerge(body.data.url as string, cookies, 1);

    await fetchWithCookiesAndMerge(
        "https://base.bestsch.com/StuOptCourse/Main",
        cookies,
        3,
    );
    return cookies;
}

export async function logout(account: Cookies): Promise<void> {
    console.log(account);
    await fetchWithCookiesAndMerge(
        "https://base.bestsch.com/BschHome/api/BschSsoCreateTicket",
        account,
    );
    await fetchWithCookiesAndMerge(
        "https://base.bestsch.com/BschHome/api/BschSsoDeleteTicket",
        account,
        1,
        "post",
        {
            "Content-Type": "application/json",
        },
        JSON.stringify({
            ticket: randomUUID(),
        }),
    );
    await fetchWithCookiesAndMerge(
        "https://sso.bestsch.com/logout?service=https://base.bestsch.com/v/home",
        account,
        1,
    );
}

export async function accountAvailable(account: Cookies): Promise<boolean> {
    DEBUG &&
        console.log(
            await (
                await fetchWithCookiesAndMerge(
                    "https://base.bestsch.com/StuOptCourse/Main",
                    account,
                )
            ).response.text(),
        );
    return (
        Math.floor(
            (
                await fetchWithCookiesAndMerge(
                    "https://base.bestsch.com/StuOptCourse/Main",
                    account,
                )
            ).response.status / 100,
        ) == 2
    );
}

export async function addClasses(
    activeId: number | string,
    lessonType: number | string,
    classes: (number | string)[],
    account: Cookies,
): Promise<{
    body: Object;
    state: boolean;
}> {
    try {
        const response = await fetch(
            "https://base.bestsch.com/StuOptCourse/api/StuOptCourse/StuOptCourse_list/OptStuAdd",
            {
                method: "POST",
                headers: {
                    Accept: "application/json, text/plain, */*",
                    "Accept-Encoding": "gzip, deflate, br, zstd",
                    "Accept-Language":
                        "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
                    Apptype: "StuOptCourse",
                    Debugapptype: "StuOptCourse",
                    "Content-Type": "application/x-www-form-urlencoded",
                    Cookie: getRawCookiesString(account),
                },
                body: new URLSearchParams({
                    ActiveID: activeId.toString(),
                    LessonType: lessonType.toString(),
                    OptSerID: classes.join(","),
                }).toString(),
            },
        );
        const body = await response.json();
        DEBUG && console.log(body);
        return {
            state: body.State,
            body,
        };
    } catch (e) {
        console.log(e);
        if (e instanceof SyntaxError) throw new Error("account unavailable");
        throw e;
    }
}
